Event Duplicate API 文档
接口功能说明
复制活动 - 基于现有活动创建新活动,同时复制关联的资源(票务、阵容、媒体等)和 Post。
[!info] 核心特性
- 复制活动元数据(标题、时间、地点、税率等)
- 可选复制票务产品、阵容、媒体、活动商品
- 自动复制关联的 Post(以 Draft 状态)
- 自动复制 StoreFront 模块配置
- 通过 Post-Sync 框架自动同步活动变更到 Post
请求地址 & 请求方式
POST /product-event/v2/duplicate
环境说明:
- Production:
https://katana-api.1m.app/product-event/v2/duplicate - Staging:
https://staging.katana-api.1m.app/product-event/v2/duplicate
请求头(Header)
| Header | 必填 | 说明 | 示例 |
|---|---|---|---|
authorization |
✅ | Bearer Token(JWT) | Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
content-type |
✅ | 请求内容类型 | application/json |
from |
❌ | 客户端标识 | client |
timezone |
❌ | 用户时区 | Asia/Shanghai |
x-track-id |
❌ | 追踪 ID | edb43640-7349-42ad-91c7-806fc07a1954 |
[!warning] 认证要求
- 必须提供有效的 JWT Token
- 用户必须是
BUSINESS_PARTNER角色或更高权限- 用户必须是源活动的所有者(
curatorId匹配)
请求参数
Body 参数
DuplicateEventRequest 继承自 CreateEventV2Request,包含所有活动创建字段,并添加复制相关字段:
必填字段
| 字段 | 类型 | 说明 |
|---|---|---|
duplicateEventId |
string (UUID) |
要复制的源活动 ID |
活动基础字段(继承自 CreateEventV2Request)
| 字段 | 类型 | 必填 | 说明 | |||
|---|---|---|---|---|---|---|
status |
EventStatus (enum) |
✅ | 活动状态:UPCOMING \ |
ON_HOLD \ |
CANCELLED \ |
COMPLETED |
title |
string |
✅ | 活动标题 | |||
allowAutoComplete |
boolean |
✅ | 是否允许自动完成 | |||
startDateDisplay |
string |
✅ | 开始时间显示(格式:YYYY-MM-DD HH:mm) |
|||
venue |
string |
✅ | 场地名称 | |||
location |
string |
✅ | 活动地点 | |||
timezone |
Timezone |
✅ | 时区配置 | |||
isTaxEnabled |
boolean |
✅ | 是否启用税费 |
可选字段
| 字段 | 类型 | 说明 | ||
|---|---|---|---|---|
endDateDisplay |
string |
结束时间显示 | ||
locationDetails |
LocationDetailsDto |
详细地址信息 | ||
poster |
ProductCoverImage |
活动封面图 | ||
note |
string |
活动备注 | ||
description |
string |
活动描述(纯文本) | ||
descriptionBodyJson |
string |
活动描述(Lexical JSON) | ||
isAddressRevealEnabled |
boolean |
是否启用地址隐私控制 | ||
addressRevealConfig |
AddressRevealConfigRequest |
地址隐私配置 | ||
taxConfig |
EventTaxConfigRequest |
税费配置 | ||
platform |
EventPlatform |
平台(PEAR \ |
SHOPIFY \ |
ALIEXPRESS) |
remoteId |
string |
外部平台 ID | ||
autoReplace |
boolean |
是否自动替换 | ||
extraInfo |
Record<string, any> |
平台特定元数据 |
可选复制字段
| 字段 | 类型 | 说明 |
|---|---|---|
tickets |
CreateProductV2Dto[] |
要复制的票务产品列表 |
eventProducts |
CreateProductV2Dto[] |
要复制的活动商品列表 |
lineup |
CreateEventLineupItemRequest[] |
要复制的阵容列表 |
medias |
CreateEventMediaItemRequest[] |
要复制的媒体列表 |
[!tip] 复制行为
- 如果不提供
tickets/eventProducts/lineup/medias,则不复制对应资源- 提供空数组
[]表示明确不复制- 提供 non-empty 数组则复制指定内容
响应结构 & 字段说明
响应结构为 EventLandingPageResponse,包含完整的活动配置和关联资源:
基础活动字段
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string |
新创建的活动 ID |
status |
EventStatus |
活动状态 |
title |
string |
活动标题 |
startDateDisplay |
string |
开始时间显示 |
location |
string |
活动地点 |
venue |
string |
场地名称 |
poster |
ProductCoverImage |
活动封面图 |
timezone |
Timezone |
时区配置 |
createdAt |
string |
创建时间 |
creationStep |
EventCreationStep |
创建流程步骤 |
税费配置
| 字段 | 类型 | 说明 | |
|---|---|---|---|
isTaxEnabled |
boolean |
是否启用税费 | |
taxConfig |
EventTaxConfig |
税费配置对象 | |
taxConfig.customTaxRate |
number |
自定义税率(0-100) | |
taxConfig.calculationType |
string |
计算类型:CUSTOM \ |
AUTOMATIC |
地址隐私配置
| 字段 | 类型 | 说明 |
|---|---|---|
isAddressRevealEnabled |
boolean |
是否启用地址隐私 |
addressRevealConfig |
AddressRevealConfig |
地址隐私配置 |
isAddressHidden |
boolean |
地址是否隐藏 |
媒体配置
| 字段 | 类型 | 说明 |
|---|---|---|
medias |
EventMediaItemResponse[] |
媒体列表 |
mediaTextConfig |
EventMediaTextConfig |
媒体文本配置 |
mediaTextConfig.headline |
string |
标题(默认:"Missed our last event?") |
mediaTextConfig.subtitle |
string |
副标题(默认:"Here's a lil recap...") |
票务和商品
| 字段 | 类型 | 说明 |
|---|---|---|
tickets |
TicketProductLite[] |
票务产品列表 |
eventProducts |
EventProductLite[] |
活动商品列表 |
阵容
| 字段 | 类型 | 说明 |
|---|---|---|
lineup |
EventLineupItemResponse[] |
阵容列表 |
统计数据
| 字段 | 类型 | 说明 |
|---|---|---|
statistics |
EventStatistics |
统计数据 |
statistics.totalTicketsAvailable |
number |
总可用票数 |
statistics.totalTicketsSold |
number |
总已售票数 |
statistics.totalTicketsRedeemed |
number |
总已核销票数 |
复制结果(新增字段)
| 字段 | 类型 | 说明 |
|---|---|---|
posts |
Post[] |
复制的 Post 列表(Draft 状态) |
storeFrontModules |
StoreFrontModule[] |
复制的 StoreFront 模块配置 |
成功示例
请求示例
curl 'https://staging.katana-api.1m.app/product-event/v2/duplicate' \
-H 'accept: application/json, text/plain, */*' \
-H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'content-type: application/json' \
--data-raw '{
"duplicateEventId": "3edd6be3-c7cd-48a9-9eb9-40f4dd4d04f1",
"status": "UPCOMING",
"title": "Event20260228 - Copy",
"allowAutoComplete": true,
"startDateDisplay": "2027-03-28",
"location": "181 Taylor Avenue, Columbus, OH",
"venue": "123",
"timezone": {
"timeZoneId": "America/New_York",
"timeZoneName": "Eastern Daylight Time",
"dstOffset": 3600,
"rawOffset": -18000
},
"isTaxEnabled": true,
"taxConfig": {
"customTaxRate": 12,
"calculationType": "CUSTOM"
},
"isAddressRevealEnabled": false,
"poster": {
"src": "https://res.cloudinary.com/dr9io1zjv/v1762842604/uploaded_images/s47jfs33fgz3do4wvrxz.png",
"width": 3072,
"height": 2048,
"position": 0,
"mediaType": "IMAGE"
},
"platform": "PEAR",
"tickets": [{
"title": "General Admission1",
"listingType": "TICKET",
"eventId": "3edd6be3-c7cd-48a9-9eb9-40f4dd4d04f1",
"variants": [{
"option1": "Default Title",
"price": "0",
"inventoryQuantity": 10,
"inventoryPolicy": "DENY",
"taxable": true
}]
}]
}'
响应示例
{
"id": "new-event-uuid",
"status": "UPCOMING",
"title": "Event20260228 - Copy",
"startDateDisplay": "2027-03-28",
"location": "181 Taylor Avenue, Columbus, OH",
"venue": "123",
"poster": {
"src": "https://res.cloudinary.com/dr9io1zjv/v1762842604/uploaded_images/s47jfs33fgz3do4wvrxz.png",
"width": 3072,
"height": 2048,
"mediaType": "IMAGE"
},
"timezone": {
"timeZoneId": "America/New_York",
"timeZoneName": "Eastern Daylight Time"
},
"isTaxEnabled": true,
"taxConfig": {
"customTaxRate": 12,
"calculationType": "CUSTOM"
},
"creationStep": "CHOOSE_MODULES",
"medias": [...],
"tickets": [{
"id": "new-ticket-uuid",
"title": "General Admission1",
"listingType": "TICKET",
"status": "ACTIVE",
"inventoryQuantity": 10,
"soldQuantity": 0,
"variants": [...]
}],
"lineup": [],
"eventProducts": [],
"statistics": {
"totalTicketsAvailable": 10,
"totalTicketsSold": 0,
"totalTicketsRedeemed": 0
},
"posts": [{
"id": "new-post-uuid",
"status": "DRAFT",
"createFromEventId": "new-event-uuid",
"syncEnabled": true,
"headline": "Event20260228 - Copy",
"relatedProducts": ["new-ticket-uuid"]
}],
"storeFrontModules": [...],
"messageInfo": {
"smsMaxLimit": 3,
"emailMaxLimit": 5,
"cooldownMinutes": 15
}
}
错误示例
401 - 未授权
{
"statusCode": 401,
"message": "Unauthorized",
"error": "Unauthorized"
}
原因:
- JWT Token 无效或过期
- Token 缺失
403 - 禁止访问
{
"statusCode": 403,
"message": "You do not have permission to duplicate this event",
"error": "Forbidden"
}
原因:
- 用户不是源活动的所有者
- 用户权限不足(非 BUSINESS_PARTNER 角色)
404 - 活动不存在
{
"statusCode": 404,
"message": "Event not found",
"error": "Not Found"
}
原因:
duplicateEventId对应的活动不存在- 活动已被软删除
400 - 请求参数错误
{
"statusCode": 400,
"message": [
"duplicateEventId must be a UUID",
"startDateDisplay must be in YYYY-MM-DD HH:mm format"
],
"error": "Bad Request"
}
原因:
- 请求参数验证失败
- 字段类型或格式不正确
500 - 服务器内部错误
{
"statusCode": 500,
"message": "Failed to duplicate event",
"error": "Internal Server Error"
}
原因:
- 数据库事务失败
- Post 创建失败(非阻塞,活动仍会创建)
- StoreFront 模块复制失败
注意事项 & 业务逻辑
复制流程
flowchart TD
A[开始: POST /duplicate] --> B[验证用户权限]
B --> C[查询源活动和关联数据]
C --> D[创建 DuplicateContext]
D --> E[开始事务]
E --> F[Stage 1: 创建新活动]
F --> G[Stage 2: 并行创建资源]
G --> H{创建成功?}
H -->|是| I[Stage 3: 复制 Posts]
H -->|否| J[回滚事务]
I --> K[Stage 4: 复制 StoreFront 模块]
J --> L[抛出异常]
K --> M[提交事务]
M --> N[发布事件同步]
N --> O[返回完整响应]
Stage 1: 创建活动
- 使用
doCreateEvent()创建新活动 - 新活动自动设置
creationStep = 'CHOOSE_MODULES' - 从请求中获取的活动字段应用于新活动
Stage 2: 并行创建资源
四个资源类型并行创建(使用 Promise.all):
| 资源类型 | 方法 | 说明 |
|---|---|---|
| Lineups | batchCreateLineups() |
复制阵容列表 |
| Medias | batchCreateMedia() |
复制媒体文件 |
| Tickets | batchCreateTickets() |
复制票务产品 |
| Event Products | batchCreateEventProducts() |
复制活动商品 |
Stage 3: 复制 Posts
- 查询源活动的所有 Posts(
status: ACTIVE) - 为每个 Post 创建副本,状态设为
DRAFT - 更新 Post 中的
createFromEventId指向新活动 - 更新 Post 中的
relatedProducts指向新票务 ID - 保留用户自定义的
headline和allowCustomizeDisplayPrices
Stage 4: 复制 StoreFront 模块
- 查询源 Posts 的 StoreFront 模块配置
- 为新 Posts 创建相同的模块配置
- 保持模块顺序和参数一致
Post-Sync 集成
- 活动复制完成后,通过
EventSyncHelper发布事件 - 触发 Post-Sync 框架自动同步活动变更到 Post
- 新 Posts 继承源 Posts 的
syncEnabled属性
回滚机制
- 任何阶段失败都会触发回滚
rollbackDuplicate()清理已创建的资源- 源活动不受影响
性能追踪
- 使用
perfTracker追踪操作性能 - 记录数据库操作次数
- 记录变更类型(event, lineup, media, ticket, event_product, storefront)
业务规则
所有权验证
- 用户必须是源活动的
curatorId - 只能复制自己拥有的活动
- Admin 用户可复制任何活动(通过 Admin API)
状态继承
- 新活动的
creationStep自动设置为CHOOSE_MODULES - 新活动的
status由请求参数决定 - 复制的票务产品保持
LIFECYCLE_STATUS_NORMAL
ID 映射
// 票务产品 ID 映射
copyFromProductIdToProductId: Map<string, string>
productIdToCopyFromProductId: Map<string, string>
用于在复制过程中跟踪旧 ID 到新 ID 的映射关系。
非阻塞设计
- Post 创建失败不会导致活动复制失败
- StoreFront 模块复制失败不会导致活动复制失败
- 只有活动自身创建失败才会回滚整个操作
相关接口
获取活动配置
GET /product-event/v2/:eventId/config
返回活动的完整配置,用于前端填充复制表单。
获取活动重复配置
GET /product-event/v2/:eventId/duplicate-config
返回活动复制的特殊配置(如票务、阵容等)。
创建活动
POST /product-event/v2
创建新活动,不基于现有活动。
参考资料
- JIRA: KAT-8883 (Event Duplication)
- JIRA: KAT-8857 (Post Integration)
- JIRA: KAT-10547 (Post Sync on Duplicate)
- 源码:
src/product-event/services/event.duplicate.service.ts - DTO:
src/product-event/v2/dto/event-v2.dto.ts - 相关文档:
src/product-event/CLAUDE.md